using System;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using Stunt.Nutss;

namespace Stunt.Nutss.Signaling
{

	/// <summary>Stub signaling client for the JSling proxy server</summary>
	public class SlingProvider : ISignalingContext
	{

		#region Variables

		///<summary> UDP socket to use for communication</summary>
		private Socket sock;

		///<summary> Address of proxy</summary>
		private IPEndPoint addr;

		///<summary> Receive buffer</summary>
		private byte[] bbuf = new byte[1472];

		#endregion

		#region Internal class: JSlingKeepalive

		/// <summary>Implements a Keepalive protocol</summary>
		private class JSlingKeepalive : IContinuation
		{

			#region Enums

			/// <summary>States of the Keepalive protocol</summary>
			class State
			{

				#region Constants

				/// <summary>Start state </summary>
				static public State START = new State(0);
				/// <summary>Keepalive message acknowledged </summary>
				static public State HELLO_SENT = new State(JSlingKeepalive.T_RETRY);
				/// <summary>Stop </summary>
				static public State RESPONSE_RCVD = new State(JSlingKeepalive.T_EXPIRE);
				/// <summary> Timeout for the associated state in milliseconds</summary>
				static public State STOP = new State(-1);

				#endregion

				#region Variables

				private int timeout;

				#endregion

				#region Constructor

				/// <summary>Creates a state with the specified timeout.</summary>
				/// <param name="timeout">timeout in milliseconds</param>
				internal State(int timeout)
				{
					this.timeout = timeout;
				}

				#endregion

				#region Properties

				/// <summary>Returns the timeout of the state</summary>
				/// <returns>timeout in milliseconds</returns>
				virtual internal int Timeout
				{
					get
					{
						return timeout;
					}
				}

				#endregion

			}

			#endregion

			#region Variables

			/// <summary>Retry timer for retransmitting lost packets.</summary>
			private const int T_RETRY = 1000;

			/// <summary>Refresh timer for keepalive.</summary>
			private const int T_EXPIRE = 59000;

			/// <summary>Current state of the protocol</summary>
			private State state;

			/// <summary>Proxy context</summary>
			private SlingProvider sig;

			/// <summary>Number of times the last message was retransmitted</summary>
			private int tries;

			/// <summary>Error to be reported to user</summary>
			private System.Exception reason = null;

			/// <summary>The ID to maintain the keepalive for</summary>
			private NutssUri id;

			/// <summary>Keepalive packet</summary>
			private SlingMessage pkt;

			#endregion

			#region Constructor

			/// <summary>Creates a new continuation for ID and socket</summary>
			/// <param name="id">ID to register</param>
			/// <param name="context">signaling channel</param>
			internal JSlingKeepalive(NutssUri id, SlingProvider context)
			{
				this.id = id;
				this.sig = context;
			}

			#endregion

			#region Init/Step/Cancel

			public virtual ISignalingMessage Init(ISignalingMessage msg)
			{

				try
				{
					// Registration message
					pkt = new SlingMessage(id, new NutssUri("registrar@nutss.net"), null);
					pkt.type = SlingMessage.Type.JSLING_REGISTER;
					state = State.START;
					return pkt;
				}

				catch (UriFormatException e)
				{
					Debug.WriteLine("JSlingKeepalive-init" + e.Message);
					return null;
				}
			}

			public virtual int Step(object o, Dispatch dispatch)
			{

				try
				{

					if (o is SlingMessage)
					{
						SlingMessage msg = (SlingMessage)o;

						if (msg.Error)
						{
							state = State.STOP;
							reason = new ExtendedSocketException("BindException");
						}

						else
						{
							state = State.RESPONSE_RCVD;

							if (msg.HasPayload)
								dispatch.Callback(this, msg.Payload);
						}
					}

					else
					{

						if (state.Timeout == State.START.Timeout || state.Timeout == State.RESPONSE_RCVD.Timeout)
						{
							sig.addr = SlingCommon.ResolveProxy(id);
							sig.Send(pkt);
							tries = 1;
							state = State.HELLO_SENT;
						}

						else if (state.Timeout == State.HELLO_SENT.Timeout)
						{

							if (tries++ < 3)
								sig.Send(pkt);

							else
							{
								state = State.STOP;
								reason = new ExtendedSocketException("Binding Timeout");
							}
						}
					}
				}

				catch (System.IO.IOException e)
				{
					Debug.WriteLine("JSlingKeepalive - step" + e.Message);
					reason = e;
					state = State.STOP;
				}
				return state.Timeout;
			}

			public virtual void Cancel(Dispatch d)
			{

				if (reason != null)
				{
					Debug.WriteLine("JSlingKeepalive - cancel" + reason.Message);
					d.Callback(this, reason);
				}
			}

			#endregion

		}

		#endregion

		#region Constructor

		/// <summary>Creates a new signaling connection</summary>
		public SlingProvider()
		{
			this.sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
		}

		#endregion

		#region RegisterChannel

		public Socket RegisterChannel()
		{

			try
			{
				sock.Blocking = false;
				//return selector.Register(sock, SelectionKey.OP_READ);
				return sock;
			}

			catch (Exception e)
			{
				Debug.WriteLine("JSlingProvider - RegisterChannel " + e.Message);
				return null;
			}
		}

		#endregion

		#region Receive

		public ISignalingMessage Receive()
		{

			try
			{
				lock (bbuf)
				{
					// Use globally preallocated buffer
					sock.Receive(bbuf);
					BinaryFormatter formatter = new BinaryFormatter();
					return (ISignalingMessage)formatter.Deserialize(new MemoryStream(bbuf));
				}
			}

			catch (Exception e)
			{
				Debug.WriteLine("SlingProvider - Receive" + e.Message);
				return null;
			}
		}

		#endregion

		#region Check

		/// <summary>Check that the SignalingMessage is a JSlingMessage</summary>
		/// <param name="msg">the message to check</param>
		/// <returns>the message cast as a JSlingMessage</returns>
		/// <throws>IllegalArgumentException if the message is not a JSlingMessage </throws>
		private SlingMessage Check(ISignalingMessage msg)
		{

			if (msg is SlingMessage)
				return (SlingMessage)msg;
			throw new ArgumentException("JSlingMessage required");
		}

		#endregion

		#region Send

		public void Send(ISignalingMessage message)
		{
			SlingMessage msg = Check(message);

			if (msg.bbuf == null)
			{
				// cache serialized version; re-used when resending the same packet
				msg.bbuf = new MemoryStream();
				BinaryFormatter formatter = new BinaryFormatter();

				formatter.Serialize(msg.bbuf, msg);
			}
			msg.bbuf.Position = 0;
			sock.SendTo(msg.bbuf.GetBuffer(), (int)msg.bbuf.Length, SocketFlags.None, addr);
			msg.bbuf = new MemoryStream(); // reset
			Console.WriteLine("sending to " + addr + ": msg=" + msg);
		}

		#endregion

		#region Register

		public IContinuation Register(NutssUri localAddress)
		{

			try
			{
				addr = SlingCommon.ResolveProxy(localAddress);
				return new JSlingKeepalive(localAddress, this);
			}

			catch (IOException e)
			{
				Debug.WriteLine("JSlingProvider - register " + e.Message);
				return null;
			}
		}

		#endregion

		#region CreateMessage

		public ISignalingMessage CreateMessage(NutssUri localAddress, NutssUri remoteAddress, object payload)
		{
			return new SlingMessage(localAddress, remoteAddress, payload);
		}

		#endregion

		#region CreateReply

		public ISignalingMessage CreateReply(ISignalingMessage msg, object payload)
		{
			SlingMessage jmsg = Check(msg);
			SlingMessage rmsg = new SlingMessage(jmsg.Destination, jmsg.Source, payload);
			rmsg.id = jmsg.id;
			return rmsg;
		}

		#endregion

		#region SetMessage

		public ISignalingMessage SetMessage(ISignalingMessage msg, object payload)
		{
			SlingMessage jmsg = Check(msg);
			jmsg.payload = payload;
			jmsg.bbuf = null; // invalidate cached serialization
			return msg;
		}

		#endregion

		#region Close

		public void Close()
		{

			try
			{
				sock.Close();
			}

			catch (IOException e)
			{
				Debug.WriteLine("JSlingProvider - close" + e.Message);
			}
		}

		#endregion

	}
}